# JVM 规范完整指南 - Java 虚拟机的编译。

本章节介绍了如何将用 Java 编程语言编写的代码编译成 Java 虚拟机指令集。

## Java 虚拟机编译（Compiling for the Java Virtual Machine）

Java 虚拟机旨在支持 Java 编程语言。Oracle 的 JDK 软件包含了一个将 Java 编程语言源代码编译到 Java 虚拟机指令集的编译器，以及一个实现了 Java 虚拟机本身的运行时系统。了解编译器如何利用 Java 虚拟机，对于未来的编译器编写者以及试图理解 Java 虚拟机本身的人都非常有用。本章中的编号章节是非规范性的。

请注意，术语"编译器"有时也指将 Java 虚拟机指令集翻译为特定 CPU 指令集的翻译器。这种翻译器的一个例子是即时编译器，它仅在 Java 虚拟机代码加载后才生成特定于平台的指令。本章不讨论与代码生成相关的问题，只讨论将 Java 编程语言源代码编译为 Java 虚拟机指令的相关问题。

### 3.1 指令格式示例（Format of Examples）

本章主要由源代码示例以及对应的 Java 虚拟机代码的注释列表组成。这些 Java 虚拟机代码是 Oracle JDK 1.0.2 版本中的 javac 编译器为这些示例所生成的，并以 Oracle JDK 中附带的 javap 工具输出的非正式"虚拟机汇编语言"形式编写。您可以使用 javap 来生成更多已编译方法的示例。

示例的格式对于任何阅读过汇编代码的人来说都应该很熟悉。每条指令的格式如下：

```text
<index> <opcode> [<operand1> [<operand2>...]] [<comment>]
```

`<index>` 是指令操作码在该方法的 Java 虚拟机代码字节数组中的索引。或者，也可以将 `<index>` 视为从方法开始处的字节偏移量。`<opcode>` 是指令操作码的助记符，零个或多个 `<operandN>` 是指令的操作数。可选的 `<comment>` 以行尾注释的语法给出：

```text
8   bipush 100     // Push int constant 100
```

注释中的部分内容是由 javap 工具自动生成的；其余部分则由作者提供。每条指令前导的 `<index>` 可以作为控制转移指令的目标。例如，一条 goto 8 指令会将控制权转移到索引为 8 的指令。需要注意的是，Java 虚拟机控制转移指令的实际操作数是相对于该指令操作码地址的偏移量；而 javap 为了便于阅读，会将这些操作数显示为（本章中也如此显示）更容易理解的、在方法内部的绝对索引。

对于表示运行时常量池索引的操作数，我们会在其前面加上一个 # 符号，并在该指令后面附上注释，用以说明所引用的运行时常量池项，例如：

```text
10  ldc #1         // Push float constant 100.0
```

或者

```text
9   invokevirtual #4    // Method Example.addTwo(II)I
```

为了方便本章的阐述，我们无需关注诸如操作数大小之类的细节。

### 3.2 常量、局部变量和控制结构的使用

Java 虚拟机代码展现出了一系列由 Java 虚拟机的设计和类型使用所决定的普遍特征。在第一个例子中，我们会遇到其中许多特征，并将对其进行较为详细的探讨。

spin 方法只是在一个空的 for 循环中循环 100 次：

```java
void spin() {
    int i;
    for (i = 0; i < 100; i++) {
        ;    // 循环体是空的
    }
}
```

编译器可能会将 spin 编译为：

```text
0 iconst_0      // 将整型常量0压入栈
1 istore_1      // 将栈顶整型值弹出并存入局部变量1（变量i）
2 goto 8        // 无条件跳转到偏移量为8的指令
5 iinc 1 1      // 将局部变量1的值增加1（i++）
8 iload_1       // 将局部变量1的值（i）压入栈
9 bipush 100    // 将整型常量100压入栈
11 if_icmplt 5  // 比较栈顶两个整型值，如果（i < 100）则跳转到偏移量为5的指令
14 return       // 从void方法返回
```

Java 虚拟机是面向栈的，大多数操作从当前栈帧的操作数栈中取出一个或多个操作数，并将结果压回操作数栈。每次调用方法时都会创建一个新栈帧，同时创建一个新的操作数栈和一组供该方法使用的局部变量（§2.6）。在计算的任一时刻，一个控制线程中很可能存在许多栈帧和同样多的操作数栈，对应于许多嵌套的方法调用。只有当前栈帧中的操作数栈是活跃的。

Java 虚拟机的指令集通过为不同数据类型的操作使用不同的字节码来区分操作数类型。spin 方法只操作 int 类型的值。其编译代码中选择用于操作类型化数据的指令（`iconst_0, istore_1, iinc, iload_1, if_icmplt`）都是专门针对 int 类型的。

spin 中的两个常量 0 和 100 使用两条不同的指令压入操作数栈。0 使用 iconst*0 指令压入，这是 `iconst*<i>` 指令族的一员。100 使用 bipush 指令压入，该指令获取其要压入的值作为一个立即操作数。

Java 虚拟机经常利用某些操作数（对于 `iconst_<i>` 指令，是 int 常量 -1、0、1、2、3、4 和 5）出现的可能性，将这些操作数隐式地包含在操作码中。因为 iconst_0 指令知道它要压入一个 int 类型的 0，所以 iconst_0 不需要存储一个操作数来告诉它要压入什么值，也不需要获取或解码操作数。将压入 0 编译为 bipush 0 也是正确的，但会使 spin 的编译代码长一个字节。一个简单的虚拟机在每次循环时还需要花费额外的时间来获取和解码显式操作数。使用隐式操作数使得编译后的代码更紧凑和高效。

spin 中的 int i 存储为 Java 虚拟机局部变量 1。因为大多数 Java 虚拟机指令操作的是从操作数栈中弹出的值，而不是直接操作局部变量，所以在为 Java 虚拟机编译的代码中，在局部变量和操作数栈之间传输值的指令很常见。这些操作在指令集中也得到了特殊支持。在 spin 中，使用 istore_1 和 iload_1 指令在局部变量之间传输值，每条指令都隐式地操作局部变量 1。istore_1 指令从操作数栈弹出一个 int 并存储到局部变量 1 中。iload_1 指令将局部变量 1 中的值压入操作数栈。

局部变量的使用（和重用）是编译器编写者的责任。专门的加载和存储指令应鼓励编译器编写者尽可能多地重用局部变量。这样产生的代码更快、更紧凑，并且在栈帧中占用的空间更少。

Java 虚拟机为局部变量的一些非常频繁的操作提供了特殊支持。iinc 指令将一个局部变量的内容增加一个单字节的有符号值。spin 中的 iinc 指令将第一个局部变量（它的第一个操作数）增加 1（它的第二个操作数）。iinc 指令在实现循环结构时非常方便。

spin 的 for 循环主要通过以下指令完成：

```text
5   iinc 1 1       // 局部变量1增加1 (i++)
8   iload_1        // 将局部变量1 (i) 压栈
9   bipush 100     // 将int常量100压栈
11  if_icmplt 5    // 比较，如果小于则循环 (i < 100)
```

bipush 指令将值 100 作为 int 压入操作数栈，然后 if_icmplt 指令将该值从操作数栈弹出并与 i 比较。如果比较成功（变量 i 小于 100），控制权转移到索引 5，for 循环的下一次迭代开始。否则，控制权传递给 if_icmplt 之后的指令。

**对其他数据类型的支持**

如果 spin 示例对循环计数器使用了 int 以外的数据类型，编译后的代码必然会发生改变以反映不同的数据类型。例如，如果 spin 示例使用 double 而不是 int，编译后的代码是：

```text
Method void dspin()
0   dconst_0       // 压入double常量0.0
1   dstore_1       // 存储到局部变量1和2
2   goto 9         // 第一次循环不递增
5   dload_1        // 从局部变量1和2加载
6   dconst_1       // 压入double常量1.0
7   dadd           // 相加；没有dinc指令
8   dstore_1       // 将结果存储到局部变量1和2
9   dload_1        // 从局部变量1和2加载
10  ldc2_w #4      // 压入double常量100.0
13  dcmpg          // 比较double值
14  iflt 5         // 如果小于则跳转 (i < 100.0)
17  return         // 完成后返回void
```

操作类型化数据的指令现在专门针对 double 类型。

请注意，double 值占用两个局部变量，尽管它们仅使用两个局部变量中较小的索引进行访问。long 类型的值也是如此。

Java 虚拟机的 1 字节操作码大小导致其编译代码非常紧凑。然而，1 字节操作码也意味着 Java 虚拟机指令集必须保持较小。作为一种折衷，Java 虚拟机并未对所有数据类型提供同等的支持：它并非完全正交。

例如，在 spin 示例的 for 语句中，int 类型值的比较可以使用一条 if_icmplt 指令实现；然而，Java 虚拟机指令集中没有一条指令可以对 double 类型的值执行条件分支。因此，dspin 必须使用一条 dcmpg 指令后跟一条 iflt 指令来实现其 double 值的比较。

Java 虚拟机为 int 类型的数据提供了最直接的支持。这部分是出于对 Java 虚拟机操作数栈和局部变量数组高效实现的预期。这也是受典型程序中 int 数据出现频率的驱动。

其他整数类型的支持较不直接。例如，没有 byte、char 或 short 版本的存储、加载或加法指令。下面是使用 short 编写的 spin 示例，它必须编译为使用 int 指令操作，并在必要时进行类型转换：

```text
Method void sspin()
0   iconst_0
1   istore_1
2   goto 10
5   iload_1        // 将short当作int处理
6   iconst_1
7   iadd
8   i2s            // 将int截断为short
9   istore_1
10  iload_1
11  bipush 100
13  if_icmplt 5
16  return
```

Java 虚拟机缺乏对 byte、char 和 short 类型的直接支持并不是特别麻烦，因为这些类型的值在内部会被提升为 int（byte 和 short 带符号扩展为 int，char 零扩展为 int）。因此，对 byte、char 和 short 数据的操作可以使用 int 指令完成。唯一的额外成本是将 int 操作的结果值截断到有效范围。

long 和浮点类型在 Java 虚拟机中具有中等程度的支持，仅缺少完整的条件控制转移指令集。

### 3.3 算术运算（Arithmetic）

Java 虚拟机通常在其操作数栈上进行算术运算。（例外是 iinc 指令，它直接增加局部变量的值。）例如，align2grain 方法将一个 int 值对齐到给定的 2 的幂：

```java
int align2grain(int i, int grain) {
    return ((i + grain-1) & ~(grain-1));
}
```

算术运算的操作数从操作数栈弹出，运算的结果被压回操作数栈。因此，算术子计算的结果可以作为其外层计算的操作数使用。例如，~(grain-1) 的计算由以下指令处理：

```text
5   iload_2        // 将 grain 压栈
6   iconst_1       // 将 int 常量 1 压栈
7   isub           // 相减；压入结果 (grain-1)
8   iconst_m1      // 将 int 常量 -1 压栈
9   ixor           // 执行按位异或；压入结果 (~(grain-1))
```

首先使用局部变量 2 的内容和一个立即整数值 1 计算 grain-1。这些操作数从操作数栈弹出，并将它们的差压回操作数栈。因此，该差值立即可用作 ixor 指令的一个操作数。（回想一下，~x == -1^x。）类似地，ixor 指令的结果成为后续 iand 指令的操作数。

整个方法的代码如下：

```text
Method int align2grain(int,int)
0   iload_1        // 加载参数 i
1   iload_2        // 加载参数 grain
2   iadd           // 计算 i + grain
3   iconst_1       // 压入常量 1
4   isub           // 计算 (i + grain) - 1
5   iload_2        // 再次加载 grain
6   iconst_1       // 压入常量 1
7   isub           // 计算 grain - 1
8   iconst_m1      // 压入常量 -1 (所有位为1)
9   ixor           // 计算 ~(grain-1)
10  iand           // 计算 ((i + grain - 1) & ~(grain-1))
11  ireturn        // 返回结果
```

### 3.4 访问运行时常量池（Accessing the Run-Time Constant Pool）

许多数值常量，以及对象、字段和方法，都是通过当前类的运行时常量池来访问的。对象访问将在后面讨论（§3.8）。int、long、float 和 double 类型的数据，以及对 String 类实例的引用，都使用 ldc、ldc_w 和 ldc2_w 指令来管理。

ldc 和 ldc_w 指令用于访问运行时常量池中非 double 和 long 类型的值（包括 String 类的实例）。只有当运行时常量池项数量很大且需要更大的索引来访问某项时，才使用 ldc_w 指令代替 ldc。ldc2_w 指令用于访问所有 double 和 long 类型的值；没有非宽（non-wide）变体。

byte、char 或 short 类型的整数常量，以及较小的 int 值，可以使用 bipush、sipush 或 `iconst_<i>` 指令（§3.2）进行编译。某些较小的浮点常量可以使用 `fconst*<f>` 和 `dconst*<d>` 指令进行编译。

在所有这些情况下，编译都是直接了当的。例如，以下代码的常量设置如下：

```java
void useManyNumeric() {
    int i = 100;
    int j = 1000000;
    long l1 = 1;
    long l2 = 0xffffffff;
    double d = 2.2;
    ... // 进行一些计算
}
```

编译结果为：

```text
Method void useManyNumeric()
0   bipush 100     // 使用 bipush 压入小的 int 常量
2   istore_1
3   ldc #1         // 使用 ldc 压入大的 int 常量 (1000000)
5   istore_2
6   lconst_1       // 很小的 long 值使用快速的小指令 lconst_1
7   lstore_3
8   ldc2_w #6      // 压入 long 值 0xffffffff (即 int 的 -1)
                   // 任何 long 常量值都可以用 ldc2_w 压入
11  lstore 5
13  ldc2_w #8      // 压入 double 常量 2.200000
                   // 不常见的 double 值也使用 ldc2_w 压入
16  dstore 7
... // 进行那些计算...
```

### 3.5 更多控制结构示例（More Control Examples）

for 语句的编译在前面已经展示过（§3.2）。Java 编程语言的大多数其他控制结构（if-then-else、do、while、break 和 continue）的编译方式也很直观。switch 语句的编译在单独的小节中处理（§3.10），异常（§3.12）和 finally 子句（§3.13）的编译也是如此。

作为进一步的例子，while 循环的编译方式也很直观，尽管 Java 虚拟机提供的具体控制转移指令因数据类型而异。像通常一样，对 int 类型数据的支持更多，例如：

```java
void whileInt() {
    int i = 0;
    while (i < 100) {
        i++;
    }
}
```

编译为：

```text
Method void whileInt()
0   iconst_0
1   istore_1
2   goto 8        // 首次循环前跳转到条件检查
5   iinc 1 1      // 循环体：i++
8   iload_1       // 条件检查开始：加载 i
9   bipush 100    // 加载 100
11  if_icmplt 5   // 如果 i < 100，跳转到循环体 (偏移5)
14  return
```

注意 while 语句的测试（使用 if_icmplt 指令实现）位于循环的 Java 虚拟机代码的底部。（之前的 spin 例子也是如此。）将测试放在循环底部，迫使在循环第一次迭代之前使用 goto 指令来跳转到测试处。如果该测试失败，循环体从未执行，那么这条额外的指令就被浪费了。然而，while 循环通常在期望其循环体被执行（通常多次执行）时使用。对于后续的迭代，将测试放在循环底部每次循环可以节省一条 Java 虚拟机指令：如果测试在循环顶部，循环体将需要一条额外的 goto 指令跳转回顶部。

涉及其他数据类型的控制结构以类似方式编译，但必须使用可用于那些数据类型的指令。这导致代码效率稍低，因为需要更多的 Java 虚拟机指令，例如：

```java
void whileDouble() {
    double i = 0.0;
    while (i < 100.1) {
        i++;
    }
}
```

编译为：

```text
Method void whileDouble()
0   dconst_0
1   dstore_1
2   goto 9        // 首次循环前跳转到条件检查
5   dload_1       // 循环体开始
6   dconst_1
7   dadd          // i++ (没有直接的 dinc 指令)
8   dstore_1
9   dload_1       // 条件检查开始：加载 i
10  ldc2_w #4     // 压入 double 常量 100.1
13  dcmpg         // 比较 double 值 (需要两条指令...)
14  iflt 5        // ... 来进行比较和分支 (如果 i < 100.1，跳转到偏移5)
17  return
```

**浮点数比较与 NaN 处理**

每种浮点类型有两条比较指令：float 类型有 fcmpl 和 fcmpg，double 类型有 dcmpl 和 dcmpg。这些变体仅在处理 NaN 时有所不同。NaN 是无序的（§2.3.2），因此如果两个操作数中有一个是 NaN，所有浮点数比较都会失败。编译器会选择适当类型的比较指令变体，使得无论比较是在非 NaN 值上失败还是遇到 NaN，都能产生相同的结果。例如：

```java
int lessThan100(double d) {
    if (d < 100.0) {
        return 1;
    } else {
        return -1;
    }
}
```

编译为：

```text
Method int lessThan100(double)
0   dload_1
1   ldc2_w #4    // 压入 double 常量 100.0
4   dcmpg        // 如果 d 是 NaN 或 d > 100.0，压入 1；
                 // 如果 d == 100.0，压入 0
5   ifge 10      // 对 0 或 1 进行分支 (即，如果栈顶值 >= 0，则分支)
8   iconst_1     // d < 100.0 且非 NaN 的情况：返回 1
9   ireturn
10  iconst_m1    // d >= 100.0 或 d 是 NaN 的情况：返回 -1
11  ireturn
```

**行为分析：**

- 如果 d 不是 NaN 且小于 100.0，dcmpg 指令将一个 int -1 压入操作数栈，ifge 指令（解释为 if (stack_top_value >= 0)）不分支，执行路径返回 1。
- 如果 d 大于 100.0 或者是 NaN，dcmpg 指令将一个 int 1 压入操作数栈，ifge 指令（if (1 >= 0)）分支到返回 -1 的路径。
- 如果 d 等于 100.0，dcmpg 指令将一个 int 0 压入操作数栈，ifge 指令（if (0 >= 0)）同样分支到返回 -1 的路径。

如果比较反过来，dcmpl 指令可以达到相同的效果：

```java
int greaterThan100(double d) {
    if (d > 100.0) {
        return 1;
    } else {
        return -1;
    }
}
```

变为：

```text
Method int greaterThan100(double)
0   dload_1
1   ldc2_w #4    // 压入 double 常量 100.0
4   dcmpl        // 如果 d 是 NaN 或 d < 100.0，压入 -1；
                 // 如果 d == 100.0，压入 0
5   ifle 10      // 对 0 或 -1 进行分支 (即，如果栈顶值 <= 0，则分支)
8   iconst_1     // d > 100.0 且非 NaN 的情况：返回 1
9   ireturn
10  iconst_m1    // d <= 100.0 或 d 是 NaN 的情况：返回 -1
11  ireturn
```

**行为分析：**

- 同样，无论比较是在非 NaN 值上失败还是因为传递了 NaN，dcmpl 指令都会将一个 int 值压入操作数栈，导致 ifle 分支。
- 如果这两条 dcmp 指令不存在，那么上面的示例方法之一将不得不做更多的工作来检测 NaN。

### 3.6 接收参数（Receiving Arguments）

如果向一个实例方法传递了 n 个参数，按照约定，它们将在为新方法调用创建的栈帧中，存储在编号为 1 到 n 的局部变量中接收。参数按照它们被传递的顺序接收。例如：

```java
int addTwo(int i, int j) {
    return i + j;
}
```

编译为：

```text
Method int addTwo(int,int)
0   iload_1        // 加载局部变量1的值 (i)
1   iload_2        // 加载局部变量2的值 (j)
2   iadd           // 相加；将int结果留在操作数栈上
3   ireturn        // 返回int结果
```

按照约定，一个实例方法会在其局部变量 0 中接收到一个对其所属实例的引用。在 Java 编程语言中，该实例可以通过 this 关键字访问。

类（静态）方法没有实例，因此对它们来说，局部变量 0 的这种用法是不必要的。一个类方法从索引 0 开始使用局部变量。如果 addTwo 方法是一个类方法，其参数的传递方式与第一个版本类似：

```java
static int addTwoStatic(int i, int j) {
    return i + j;
}
```

编译为：

```text
Method int addTwoStatic(int,int)
0   iload_0        // 加载局部变量0的值 (i)
1   iload_1        // 加载局部变量1的值 (j)
2   iadd           // 相加
3   ireturn        // 返回int结果
```

唯一的区别在于，方法参数从局部变量 0 开始，而不是从 1 开始。

### 3.7. 方法调用（Invoking Methods）

对实例方法的正常方法调用会基于对象的运行时类型进行分派。（用 C++ 的术语来说，它们是虚函数。）这样的调用通过 invokevirtual 指令实现，该指令的参数是一个运行时常量池项的索引，该索引给出了对象类类型的二进制名称的内部形式、要调用的方法的名称以及该方法的描述符（§4.3.3）。要调用之前定义为实例方法的 addTwo 方法，我们可以这样写：

```java
int add12and13() {
    return addTwo(12, 13);
}
```

这被编译为：

```text
Method int add12and13()
0   aload_0             // 加载局部变量 0 (this)
1   bipush 12           // 压入 int 常量 12
3   bipush 13           // 压入 int 常量 13
5   invokevirtual #4    // 调用方法 Example.addtwo(II)I
8   ireturn             // 返回操作数栈顶的 int 值；
                        // 它是 addTwo() 的 int 结果
```

调用过程是这样设置的：首先将对当前实例 this 的引用压入操作数栈。然后压入方法调用的参数，即 int 值 12 和 13。当为 addTwo 方法创建栈帧时，传递给该方法的参数成为新栈帧局部变量的初始值。也就是说，调用者压入操作数栈的 this 引用和两个参数，将成为被调用方法的局部变量 0、1 和 2 的初始值。

最后，调用 addTwo。当它返回时，其 int 返回值被压入调用者（即 add12and13 方法）栈帧的操作数栈。因此，返回值被置于可立即返回给 add12and13 调用者的位置。

add12and13 的返回由其 ireturn 指令处理。ireturn 指令获取当前帧操作数栈上由 addTwo 返回的 int 值，并将其压入调用者帧的操作数栈。然后将控制权返回给调用者，使调用者的帧成为当前帧。Java 虚拟机为其许多数值和引用数据类型提供了不同的返回指令，还为无返回值的方法提供了 return 指令。同一组返回指令用于所有种类的方法调用。

invokevirtual 指令的操作数（在示例中为运行时常量池索引 #4）并不是方法在类实例中的偏移量。编译器不知道类实例的内部布局。相反，它生成对实例方法的符号引用，这些引用存储在运行时常量池中。这些运行时常量池项在运行时被解析以确定实际的方法位置。对于所有其他访问类实例的 Java 虚拟机指令也是如此。

调用 addTwoStatic（addTwo 的类（静态）变体）是类似的，如下所示：

```java
int add12and13() {
    return addTwoStatic(12, 13);
}
```

尽管使用了不同的 Java 虚拟机方法调用指令：

```text
Method int add12and13()
0   bipush 12
2   bipush 13
4   invokestatic #3     // 调用方法 Example.addTwoStatic(II)I
7   ireturn
```

编译对类（静态）方法的调用与编译对实例方法的调用非常相似，只是调用者不传递 this。因此，方法参数将从局部变量 0 开始接收（§3.6）。invokestatic 指令总是用于调用类方法。

invokespecial 指令必须用于调用实例初始化方法（§3.8）。在调用超类（super）中的方法时也会使用它。例如，给定如下声明的 Near 和 Far 类：

```java
class Near {
    int it;
    int getItNear() {
        return it;
    }
}
class Far extends Near {
    int getItFar() {
        return super.getItNear();
    }
}
```

方法 Far.getItFar（它调用超类方法）变为：

```text
Method int getItFar()
0   aload_0             // 加载 this
1   invokespecial #4    // 调用方法 Near.getItNear()I
4   ireturn
```

请注意，使用 invokespecial 指令调用的方法总是将 this 作为第一个参数传递给被调用的方法。像往常一样，它在局部变量 0 中接收。

为了调用方法句柄的目标，编译器必须形成一个记录实际参数和返回类型的方法描述符。编译器不能对参数执行方法调用转换；相反，它必须根据参数自身的未转换类型将它们压入栈。编译器安排在对方法句柄对象的引用在参数之前压入栈。编译器发出一个 invokevirtual 指令，该指令引用一个描述参数和返回类型的描述符。通过与方法解析的特殊约定（§5.4.3.3），一个调用 java.lang.invoke.MethodHandle 的 invokeExact 或 invoke 方法的 invokevirtual 指令将总是能够链接，只要方法描述符在句法上是格式良好的并且描述符中命名的类型可以被解析。

### 3.8 类实例工作（Working with Class Instances）

Java 虚拟机类实例是使用 Java 虚拟机的 new 指令创建的。回想一下，在 Java 虚拟机层面，构造器表现为一个具有编译器提供的名称 `<init>` 的方法。这个特殊命名的方法被称为实例初始化方法（§2.9）。一个给定的类可能存在多个实例初始化方法，对应于多个构造器。一旦类实例被创建，并且其实例变量（包括该类及其所有超类的实例变量）已初始化为其默认值，就会调用新类实例的一个实例初始化方法。例如：

```java
Object create() {
    return new Object();
}
```

编译为：

```text
Method java.lang.Object create()
0   new #1              // 类 java.lang.Object
3   dup                 // 复制栈顶引用
4   invokespecial #4    // 方法 java.lang.Object.<init>()V
7   areturn             // 返回对象引用
```

类实例的传递和返回（作为引用类型）与数值非常相似，尽管引用类型有自己的一套指令，例如：

```java
int i;                                  // 一个实例变量
MyObj example() {
    MyObj o = new MyObj();
    return silly(o);
}
MyObj silly(MyObj o) {
    if (o != null) {
        return o;
    } else {
        return o;
    }
}
```

变为：

```text
Method MyObj example()
0   new #2              // 类 MyObj
3   dup
4   invokespecial #5    // 方法 MyObj.<init>()V
7   astore_1            // 将引用存储到局部变量 1 (o)
8   aload_0             // 加载 this (用于调用 silly)
9   aload_1             // 加载局部变量 1 (o) (silly 的参数)
10  invokevirtual #4    // 方法 Example.silly(LMyObj;)LMyObj;
13  areturn             // 返回 silly 的结果

Method MyObj silly(MyObj)
0   aload_1             // 加载参数 o
1   ifnull 6            // 如果为 null，跳转到偏移 6
4   aload_1             // 非 null 分支：加载 o
5   areturn             // 返回 o
6   aload_1             // null 分支：加载 o
7   areturn             // 返回 o (总是返回 o，条件无实际效果)
```

类实例的字段（实例变量）使用 getfield 和 putfield 指令访问。如果 i 是一个 int 类型的实例变量，定义的方法 setIt 和 getIt：

```java
void setIt(int value) {
    i = value;
}
int getIt() {
    return i;
}
```

变为：

```text
Method void setIt(int)
0   aload_0             // 加载 this 引用
1   iload_1             // 加载参数 value
2   putfield #4         // 字段 Example.i I (将值存入字段 i)
5   return

Method int getIt()
0   aload_0             // 加载 this 引用
1   getfield #4         // 字段 Example.i I (从字段 i 加载值)
4   ireturn             // 返回 int 值
```

与方法调用指令的操作数一样，putfield 和 getfield 指令的操作数（运行时常量池索引 #4）也不是字段在类实例中的偏移量。编译器生成对实例字段的符号引用，这些引用存储在运行时常量池中。这些运行时常量池项在运行时被解析，以确定字段在引用对象中的位置。

### 3.9 数组 （Arrays）

Java 虚拟机数组也是对象。数组使用一组独特的指令来创建和操作。newarray 指令用于创建数值类型的数组。以下代码：

```java
void createBuffer() {
    int buffer[];
    int bufsz = 100;
    int value = 12;
    buffer = new int[bufsz];
    buffer[10] = value;
    value = buffer[11];
}
```

可能被编译为：

```text
Method void createBuffer()
0   bipush 100     // 压入 int 常量 100 (bufsz)
2   istore_2       // 将 bufsz 存储到局部变量 2
3   bipush 12      // 压入 int 常量 12 (value)
5   istore_3       // 将 value 存储到局部变量 3
6   iload_2        // 压入 bufsz...
7   newarray int   // ...并创建该长度的新 int 数组
9   astore_1       // 将新数组存储到 buffer
10  aload_1        // 压入 buffer
11  bipush 10      // 压入 int 常量 10
13  iload_3        // 压入 value
14  iastore        // 将 value 存储到 buffer[10]
15  aload_1        // 压入 buffer
16  bipush 11      // 压入 int 常量 11
18  iaload         // 压入 buffer[11] 的值...
19  istore_3       // ...并将其存储到 value
20  return
```

anewarray 指令用于创建一维对象引用数组，例如：

```java
void createThreadArray() {
    Thread threads[];
    int count = 10;
    threads = new Thread[count];
    threads[0] = new Thread();
}
```

变为：

```text
Method void createThreadArray()
0   bipush 10           // 压入 int 常量 10
2   istore_2            // 将 count 初始化为该值
3   iload_2             // 压入 count，供 anewarray 使用
4   anewarray class #1  // 创建 Thread 类的新数组
7   astore_1            // 将新数组存储到 threads
8   aload_1             // 压入 threads 的值
9   iconst_0            // 压入 int 常量 0
10  new #1              // 创建 Thread 类的实例
13  dup                 // 复制引用...
14  invokespecial #5    // ...用于 Thread 的构造器
                        // 方法 java.lang.Thread.<init>()V
17  aastore             // 将新 Thread 存储到数组索引 0 处
18  return
```

anewarray 指令也可用于创建多维数组的第一维。或者，可以使用 multianewarray 指令一次创建多个维度。例如，三维数组：

```java
int[][][] create3DArray() {
    int grid[][][];
    grid = new int[10][5][];
    return grid;
}
```

由以下代码创建：

```text
Method int create3DArray()[][][]
0   bipush 10                // 压入 int 10 (第一维)
2   iconst_5                 // 压入 int 5 (第二维)
3   multianewarray #1 dim #2 // 类 [[[I, 一个三维 int 数组；
                             // 仅创建前两个维度
7   astore_1                 // 存储新数组...
8   aload_1                  // ...然后准备返回它
9   areturn
```

multianewarray 指令的第一个操作数是要创建的数组类类型的运行时常量池索引。第二个是该数组类型实际要创建的维度数。multianewarray 指令可用于创建该类型的所有维度，如 create3DArray 的代码所示。请注意，多维数组只是一个对象，因此分别由 aload_1 和 areturn 指令加载和返回。有关数组类名的信息，请参见 §4.4.1。

所有数组都有关联的长度，通过 arraylength 指令访问。

### 3.10 编译 Switch 语句 （Compiling Switches）

switch 语句的编译使用 tableswitch 和 lookupswitch 指令。当 switch 的 case 可以高效地表示为目标偏移量表（跳转表）中的索引时，使用 tableswitch 指令。如果 switch 表达式的值落在有效索引范围之外，则使用 switch 的默认目标。例如：

```java
int chooseNear(int i) {
    switch (i) {
        case 0:  return  0;
        case 1:  return  1;
        case 2:  return  2;
        default: return -1;
    }
}
```

编译为：

```text
Method int chooseNear(int)
0   iload_1             // 加载局部变量 1 (参数 i)
1   tableswitch 0 to 2: // 有效索引范围是 0 到 2
      0: 28             // 如果 i 是 0，跳转到偏移 28
      1: 30             // 如果 i 是 1，跳转到偏移 30
      2: 32             // 如果 i 是 2，跳转到偏移 32
      default:34        // 否则，跳转到偏移 34
28  iconst_0            // i 是 0；压入 int 常量 0...
29  ireturn             // ...并返回它
30  iconst_1            // i 是 1；压入 int 常量 1...
31  ireturn             // ...并返回它
32  iconst_2            // i 是 2；压入 int 常量 2...
33  ireturn             // ...并返回它
34  iconst_m1           // 否则压入 int 常量 -1...
35  ireturn             // ...并返回它
```

Java 虚拟机的 tableswitch 和 lookupswitch 指令仅操作 int 类型数据。因为对 byte、char 或 short 值的操作在内部被提升为 int，所以一个表达式计算结果为这些类型之一的 switch 语句，会被当作计算结果为 int 类型来编译。如果 chooseNear 方法是使用 short 类型编写的，也会生成与使用 int 类型时相同的 Java 虚拟机指令。其他数值类型必须窄化为 int 类型才能在 switch 中使用。

当 switch 的 case 比较稀疏时，tableswitch 指令的表表示在空间上变得低效。这时可以使用 lookupswitch 指令。lookupswitch 指令将 int 键（case 标签的值）与目标偏移量在表中配对。当执行 lookupswitch 指令时，将 switch 表达式的值与表中的键进行比较。如果有一个键与表达式的值匹配，则继续在关联的目标偏移量处执行。如果没有键匹配，则在默认目标处继续执行。例如，以下代码的编译结果：

```java
int chooseFar(int i) {
    switch (i) {
        case -100: return -1;
        case 0:    return  0;
        case 100:  return  1;
        default:   return -1;
    }
}
```

看起来与 chooseNear 的代码非常相似，除了使用 lookupswitch 指令：

```text
Method int chooseFar(int)
0   iload_1
1   lookupswitch 3:     // 表中有 3 个键值对
         -100: 36       // 如果 i == -100，跳转到 36
            0: 38       // 如果 i == 0，跳转到 38
          100: 40       // 如果 i == 100，跳转到 40
      default: 42       // 否则，跳转到 42
36  iconst_m1
37  ireturn
38  iconst_0
39  ireturn
40  iconst_1
41  ireturn
42  iconst_m1
43  ireturn
```

Java 虚拟机规定 lookupswitch 指令的表必须按键排序，以便实现可以使用比线性扫描更高效的搜索算法。即便如此，lookupswitch 指令必须搜索其键以寻找匹配项，而不能像 tableswitch 那样简单地执行边界检查并索引到表中。因此，在空间考虑允许选择的情况下，tableswitch 指令可能比 lookupswitch 更高效。

### 3.11 操作数栈操作（Operations on the Operand Stack）

```java
public long nextIndex() {
    return index++;
}

private long index = 0;
```

编译为：

```text
Method long nextIndex()
0   aload_0        // 压入 this
1   dup            // 复制它（栈顶现在有两个 this 引用）
2   getfield #4    // 消耗一个 this 副本，压入 long 字段 index 的值，
                   // 此时栈顶为 index 值，其下是原始 this 引用
5   dup2_x1        // 将操作数栈顶的 long 值（index）插入到原始 this 引用之下
                   // 操作后栈结构（自顶向下）：index 副本, this, index 原始值
6   lconst_1       // 压入 long 常量 1
7   ladd           // index 原始值加 1...
8   putfield #4    // ...并将结果存储到字段中（消耗 this 和相加结果）
11  lreturn        // index 的原始值在操作数栈顶，准备返回
```

请注意，Java 虚拟机绝不允许其操作数栈操作指令修改或分解操作数栈上的单个值。

### 3.12 抛出和处理异常（Throwing and Handling Exceptions）

程序使用 throw 关键字抛出异常。其编译很简单：

```java
void cantBeZero(int i) throws TestExc {
    if (i == 0) {
        throw new TestExc();
    }
}
```

变为：

```text
Method void cantBeZero(int)
0   iload_1             // 加载参数 1 (i)
1   ifne 12             // 如果 i != 0，跳转到 12 (正常返回)
4   new #1              // 创建 TestExc 实例
7   dup                 // 一个引用传给其构造器
8   invokespecial #7    // 方法 TestExc.<init>()V
11  athrow              // 第二个引用被抛出
12  return              // 如果抛出了 TestExc，永远不会到达这里
```

try-catch 结构的编译是直截了当的。例如：

```java
void catchOne() {
    try {
        tryItOut();
    } catch (TestExc e) {
        handleExc(e);
    }
}
```

被编译为：

```text
Method void catchOne()
0   aload_0             // try 块开始
1   invokevirtual #6    // 方法 Example.tryItOut()V
4   return              // try 块结束；正常返回
5   astore_1            // 将抛出的值存储到局部变量 1
6   aload_0             // 压入 this
7   aload_1             // 压入抛出的值
8   invokevirtual #5    // 调用处理方法：
                        // Example.handleExc(LTestExc;)V
11  return              // 处理 TestExc 后返回

异常表：
起始   结束    目标      类型
0       4       5       类 TestExc
```

仔细看，try 块的编译就像没有 try 一样：

```text
Method void catchOne()
0   aload_0             // try 块开始
1   invokevirtual #6    // 方法 Example.tryItOut()V
4   return              // try 块结束；正常返回
```

如果在 try 块执行期间没有抛出异常，其行为就像没有 try 一样：调用 tryItOut 然后 catchOne 返回。

跟在 try 块后面的是实现单个 catch 子句的 Java 虚拟机代码：

```text
5   astore_1            // 将抛出的值存储到局部变量 1
6   aload_0             // 压入 this
7   aload_1             // 压入抛出的值
8   invokevirtual #5    // 调用处理方法：
                        // Example.handleExc(LTestExc;)V
11  return              // 处理 TestExc 后返回

异常表：
起始   结束    目标      类型
0       4       5       类 TestExc
```

handleExc 的调用（catch 子句的内容）也像普通方法调用一样被编译。然而，catch 子句的存在导致编译器生成一个异常表条目（§2.10, §4.7.3）。catchOne 方法的异常表有一个条目，对应于 catchOne 的 catch 子句可以处理的一个参数（类 TestExc 的实例）。如果在 catchOne 中索引 0 到 4 之间的指令执行期间抛出了某个是 TestExc 实例的值，控制权将转移到索引 5 处的 Java 虚拟机代码，该代码实现了 catch 子句的块。如果抛出的值不是 TestExc 的实例，catchOne 的 catch 子句无法处理它。相反，该值被重新抛给 catchOne 的调用者。

一个 try 可以有多个 catch 子句：

```java
void catchTwo() {
    try {
        tryItOut();
    } catch (TestExc1 e) {
        handleExc(e);
    } catch (TestExc2 e) {
        handleExc(e);
    }
}
```

给定 try 语句的多个 catch 子句的编译方式很简单，就是将每个 catch 子句的 Java 虚拟机代码逐个追加，并向异常表添加条目，如下所示：

```text
Method void catchTwo()
0   aload_0             // 开始 try 块
1   invokevirtual #5    // 方法 Example.tryItOut()V
4   return              // try 块结束；正常返回
5   astore_1            // 开始 TestExc1 的处理程序；
                        // 存储抛出的值到局部变量 1
6   aload_0             // 压入 this
7   aload_1             // 压入抛出的值
8   invokevirtual #7    // 调用处理方法：
                        // Example.handleExc(LTestExc1;)V
11  return              // 处理 TestExc1 后返回
12  astore_1            // 开始 TestExc2 的处理程序；
                        // 存储抛出的值到局部变量 1
13  aload_0             // 压入 this
14  aload_1             // 压入抛出的值
15  invokevirtual #7    // 调用处理方法：
                        // Example.handleExc(LTestExc2;)V
18  return              // 处理 TestExc2 后返回

异常表：
起始   结束    目标      类型
0       4       5       类 TestExc1
0       4       12      类 TestExc2
```

如果在 try 子句执行期间（索引 0 到 4 之间）抛出的值与一个或多个 catch 子句的参数匹配（该值是一个或多个参数的实例），则选择第一个（最内层的）这样的 catch 子句。控制权转移到该 catch 子句块对应的 Java 虚拟机代码。如果抛出的值与 catchTwo 的任何 catch 子句的参数都不匹配，Java 虚拟机将重新抛出该值，而不调用 catchTwo 的任何 catch 子句中的代码。

嵌套的 try-catch 语句的编译方式非常类似于具有多个 catch 子句的 try 语句：

```java
void nestedCatch() {
    try {
        try {
            tryItOut();
        } catch (TestExc1 e) {
            handleExc1(e);
        }
    } catch (TestExc2 e) {
        handleExc2(e);
    }
}
```

变为：

```text
Method void nestedCatch()
0   aload_0             // 开始 try 块
1   invokevirtual #8    // 方法 Example.tryItOut()V
4   return              // try 块结束；正常返回
5   astore_1            // 开始 TestExc1 的处理程序；
                        // 存储抛出的值到局部变量 1
6   aload_0             // 压入 this
7   aload_1             // 压入抛出的值
8   invokevirtual #7    // 调用处理方法：
                        // Example.handleExc1(LTestExc1;)V
11  return              // 处理 TestExc1 后返回
12  astore_1            // 开始 TestExc2 的处理程序；
                        // 存储抛出的值到局部变量 1
13  aload_0             // 压入 this
14  aload_1             // 压入抛出的值
15  invokevirtual #6    // 调用处理方法：
                        // Example.handleExc2(LTestExc2;)V
18  return              // 处理 TestExc2 后返回

异常表：
起始   结束    目标      类型
0       4       5       类 TestExc1
0       12      12      类 TestExc2
```

catch 子句的嵌套仅在异常表中表示。Java 虚拟机不强制异常表条目的嵌套或任何顺序（§2.10）。然而，因为 try-catch 结构是结构化的，编译器总是可以排序异常处理程序表的条目，使得对于任何抛出的异常和该方法中的任何程序计数器值，第一个匹配所抛出异常的异常处理程序对应于最内层的匹配 catch 子句。

例如，如果 tryItOut 的调用（在索引 1 处）抛出了一个 TestExc1 的实例，它将被调用 handleExc1 的 catch 子句处理。即使异常发生在外部 catch 子句（捕获 TestExc2）的范围内，并且即使该外部 catch 子句本来可能能够处理抛出的值，也是如此。

作为一个细微之处，请注意 catch 子句的范围在"起始"端是包含的，在"结束"端是排除的（§4.7.3）。因此，捕获 TestExc1 的 catch 子句的异常表条目不覆盖偏移量 4 处的 return 指令。然而，捕获 TestExc2 的 catch 子句的异常表条目覆盖了偏移量 11 处的 return 指令。嵌套 catch 子句中的 return 指令包含在嵌套 catch 子句覆盖的指令范围内。

### 3.13 编译 finally（Compiling finally）

（本节假设编译器生成的类文件版本号为 50.0 或以下，因此可能使用 jsr 指令。另请参见 §4.10.2.5。）

try-finally 语句的编译与 try-catch 类似。在将控制转移到 try 语句之外之前，无论该转移是正常的还是突然的（因为抛出了异常），都必须首先执行 finally 子句。对于这个简单的例子：

```java
void tryFinally() {
    try {
        tryItOut();
    } finally {
        wrapItUp();
    }
}
```

编译后的代码是：

```text
Method void tryFinally()
0   aload_0             // try 块开始
1   invokevirtual #6    // 方法 Example.tryItOut()V
4   jsr 14              // 调用 finally 块
7   return              // try 块结束
8   astore_1            // 开始处理任何抛出的异常
9   jsr 14              // 调用 finally 块
12  aload_1             // 压入抛出的值
13  athrow              // ...并将值重新抛给调用者
14  astore_2            // finally 块开始
15  aload_0             // 压入 this
16  invokevirtual #5    // 方法 Example.wrapItUp()V
19  ret 2               // 从 finally 块返回

异常表：
起始   结束    目标      类型
0       4       8           any
```

控制离开 try 语句有四种方式：通过执行到该块的底部、通过返回、通过执行 break 或 continue 语句，或者通过引发异常。如果 tryItOut 返回而没有引发异常，则使用 jsr 指令将控制转移到 finally 块。索引 4 处的 jsr 14 指令对索引 14 处的 finally 块代码进行"子程序调用"（finally 块被编译为一个嵌入式子程序）。当 finally 块完成时，ret 2 指令将控制返回到索引 4 处 jsr 指令之后的指令。

更详细地说，子程序调用的工作方式如下：jsr 指令在跳转之前将下一条指令（索引 7 处的 return）的地址压入操作数栈。作为跳转目标的 astore_2 指令将该地址从操作数栈存储到局部变量 2 中。运行 finally 块的代码（在本例中为 aload_0 和 invokevirtual 指令）。假设该代码的执行正常完成，ret 指令从局部变量 2 检索地址并在该地址处恢复执行。执行 return 指令，tryFinally 正常返回。

带有 finally 子句的 try 语句被编译为具有一个特殊的异常处理程序，该处理程序可以处理在 try 语句内抛出的任何异常。如果 tryItOut 抛出异常，则在 tryFinally 的异常表中搜索适当的异常处理程序。找到特殊处理程序，导致执行在索引 8 处继续。索引 8 处的 astore_1 指令将抛出的值存储到局部变量 1 中。随后的 jsr 指令对 finally 块的代码进行子程序调用。假设该代码正常返回，索引 12 处的 aload_1 指令将抛出的值压回操作数栈，随后的 athrow 指令重新抛出该值。

编译同时具有 catch 子句和 finally 子句的 try 语句更为复杂：

```java
void tryCatchFinally() {
    try {
        tryItOut();
    } catch (TestExc e) {
        handleExc(e);
    } finally {
        wrapItUp();
    }
}
```

变为：

```text
Method void tryCatchFinally()
0   aload_0             // try 块开始
1   invokevirtual #4    // 方法 Example.tryItOut()V
4   goto 16             // 跳转到 finally 块
7   astore_3            // 开始 TestExc 的处理程序；
                        // 存储抛出的值到局部变量 3
8   aload_0             // 压入 this
9   aload_3             // 压入抛出的值
10  invokevirtual #6    // 调用处理方法：
                        // Example.handleExc(LTestExc;)V
13  goto 16             // 这个 goto 是多余的，但是由
                        // JDK 1.0.2 中的 javac 生成
16  jsr 26              // 调用 finally 块
19  return              // 处理 TestExc 后返回
20  astore_1            // 开始处理 TestExc 以外的异常，
                        // 或处理 TestExc 时抛出的异常
21  jsr 26              // 调用 finally 块
24  aload_1             // 压入抛出的值...
25  athrow              // ...并将值重新抛给调用者
26  astore_2            // finally 块开始
27  aload_0             // 压入 this
28  invokevirtual #5    // 方法 Example.wrapItUp()V
31  ret 2               // 从 finally 块返回

异常表：
起始   结束    目标      类型
0       4       7           类 TestExc
0       16      20          any
```

如果 try 语句正常完成，索引 4 处的 goto 指令跳转到索引 16 处对 finally 块的子程序调用。执行索引 26 处的 finally 块，控制返回到索引 19 处的 return 指令，tryCatchFinally 正常返回。

如果 tryItOut 抛出一个 TestExc 实例，则选择异常表中第一个（最内层的）适用的异常处理程序来处理该异常。该异常处理程序的代码从索引 7 开始，将抛出的值传递给 handleExc，并在其返回时与正常情况一样对索引 26 处的 finally 块进行相同的子程序调用。如果 handleExc 没有抛出异常，tryCatchFinally 正常返回。

如果 tryItOut 抛出的值不是 TestExc 的实例，或者 handleExc 本身抛出异常，则该情况由异常表中的第二个条目处理，该条目处理在索引 0 到 16 之间抛出的任何值。该异常处理程序将控制转移到索引 20，在那里抛出的值首先存储到局部变量 1 中。索引 26 处的 finally 块代码作为子程序被调用。如果它返回，则从局部变量 1 中检索抛出的值并使用 athrow 指令重新抛出。如果在 finally 子句执行期间抛出了新值，则 finally 子句中止，并且 tryCatchFinally 突然返回，将新值抛给其调用者。

### 3.14 同步（Synchronization）

Java 虚拟机中的同步通过监视器的进入和退出来实现，既可以显式地（通过使用 monitorenter 和 monitorexit 指令），也可以隐式地（通过方法调用和返回指令）。

对于用 Java 编程语言编写的代码，也许最常见的同步形式是同步方法。同步方法通常不是使用 monitorenter 和 monitorexit 来实现的。相反，它仅仅在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分，该方法调用指令会检查此标志（§2.11.10）。

monitorenter 和 monitorexit 指令使得能够编译同步语句。例如：

```java
void onlyMe(Foo f) {
    synchronized(f) {
        doSomething();
    }
}
```

被编译为：

```text
Method void onlyMe(Foo)
0   aload_1             // 压入 f
1   dup                 // 在栈上复制它
2   astore_2            // 将副本存储到局部变量 2
3   monitorenter        // 进入与 f 关联的监视器
4   aload_0             // 持有监视器，传递 this 和...
5   invokevirtual #5    // ...调用 Example.doSomething()V
8   aload_2             // 压入局部变量 2 (f)
9   monitorexit         // 退出与 f 关联的监视器
10  goto 18             // 正常完成方法
13  astore_3            // 如果发生任何抛出，最终到达这里
14  aload_2             // 压入局部变量 2 (f)
15  monitorexit         // 确保退出监视器！
16  aload_3             // 压入抛出的值...
17  athrow              // ...并将值重新抛给调用者
18  return              // 正常情况返回

异常表：
起始   结束    目标      类型
4       10      13          any
13      16      13          any
```

编译器确保在任何方法调用完成时，对于自方法调用以来执行的每个 monitorenter 指令，都会有一条 monitorexit 指令被执行。无论方法调用是正常完成（§2.6.4）还是突然完成（§2.6.5），都是如此。为了在方法调用突然完成时强制正确配对 monitorenter 和 monitorexit 指令，编译器会生成异常处理程序（§2.10），这些处理程序将匹配任何异常，并且其关联的代码执行必要的 monitorexit 指令。

### 3.15 注解（Annotations）

类文件中注解的表示在 §4.7.16-§4.7.22 中描述。这些章节清楚地说明了如何在类、接口、字段、方法、方法参数和类型参数的声明上表示注解，以及在这些声明中使用的类型上的注解。包声明上的注解需要额外的规则，在此给出。

当编译器遇到一个必须在运行时可用的带注解的包声明时，它会生成一个具有以下属性的类文件：

- 该类文件表示一个接口，即 ClassFile 结构的 ACC_INTERFACE 和 ACC_ABSTRACT 标志被设置（§4.1）。
- 如果类文件版本号低于 50.0，则 ACC_SYNTHETIC 标志未被设置；如果类文件版本号为 50.0 或以上，则 ACC_SYNTHETIC 标志被设置。
- 该接口具有包访问权限（JLS §6.6.1）。
- 该接口的名称是 package-name.package-info 的内部形式（§4.2.1）。
- 该接口没有超接口。
- 该接口的唯一成员是《Java 语言规范（Java SE 25 版）》（JLS §9.2）所隐含的那些成员。
- 包声明上的注解作为 RuntimeVisibleAnnotations 和 RuntimeInvisibleAnnotations 属性存储在 ClassFile 结构的属性表中。

### 3.16 模块（Modules）

包含模块声明（JLS §7.7）的编译单元被编译成一个包含 Module 属性的类文件。

按照约定，包含模块声明的编译单元的名称是 module-info.java，这与包含纯包声明的编译单元约定 package-info.java 相呼应。因此，按照约定，模块声明编译后的形式名称是 module-info.class。

ClassFile 结构的 access_flags 项中的一个标志 ACC_MODULE (0x8000)，表明该类文件声明了一个模块。ACC_MODULE 与 ACC_ANNOTATION (0x2000) 和 ACC_ENUM (0x4000) 类似，都用于标记该类文件"不是一个普通的类"。ACC_MODULE 不描述类或接口的可访问性。

Module 属性明确说明了模块的依赖关系；在 ClassFile 级别没有隐式的 requires 指令。如果 requires_count 项为零，则 Java SE 平台不会推断存在 requires 表或其内的任何特定条目。java.base 是唯一允许 requires_count 为零的模块，因为它是初始模块。对于其他每个模块，Module 属性必须至少有一个长度为 1 的 requires 表，因为其他每个模块都依赖于 java.base。如果一个编译单元包含一个模块声明（除了 java.base）没有明确声明其对 java.base 的依赖，那么编译器必须在 requires 表中为 java.base 生成一个条目，并将其标记为 ACC_MANDATED 以表示它是隐式声明的。

为了封装，Module 属性明确说明了普通模块导出和开放的包；对于普通模块，在 ClassFile 级别没有隐式的 exports 或 opens 指令。如果 exports_count 项或 opens_count 项为零，则 Java SE 平台不会推断存在 exports 表或 opens 表，或其内的任何特定条目。另一方面，对于开放模块，Module 属性对于模块开放的包是隐式的。开放模块的所有包都向所有其他模块开放，即使 opens_count 项为零。

Module 属性明确说明了模块对服务的使用和提供；在 ClassFile 级别没有隐式的 uses 或 provides 指令。
